// 移动摄像机
// Created by denglibin on 2021/4/3 0003.
//
/**
 * OpenGL是一个右手坐标系
 * 为了理解为什么被称为右手坐标系，按如下的步骤做：

沿着正y轴方向伸出你的右臂，手指着上方。
大拇指指向右方。
食指指向上方。
中指向下弯曲90度。
如果你的动作正确，那么你的大拇指指向正x轴方向，食指指向正y轴方向，中指指向正z轴方向。如果你用左臂来做这些动作，你会发现z轴的方向是相反的。
 这个叫做左手坐标系，它被DirectX广泛地使用。注意在标准化设备坐标系中OpenGL实际上使用的是左手坐标系（投影矩阵交换了左右手）
 **/

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include "shader.h"
#include "window.h"
#include "gl_object.h"
#include "cglm.h"
#include "utils.h"
// settings
static const  int width = 800;
static const  int height = 600;
//顶点坐标
static float vertices[] = {
        //位置坐标                纹理坐标
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
        0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,

        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
        0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,

        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f
};
//10个立方体的位置
static vec3 cubePositions[10] = {
        {0.0f,  0.0f,  0.0f},
        {2.0f,  5.0f, -15.0f},
        {-1.5f, -2.2f, -2.5f},
        {-3.8f, -2.0f, -12.3f},
        {2.4f, -0.4f, -3.5f},

        {-1.7f,  3.0f, -7.5f},
        { 1.3f, -2.0f, -2.5f},
        {1.5f,  2.0f, -2.5f},
        {1.5f,  0.2f, -1.5f},
        {-1.3f,  1.0f, -1.5f}
};
static vec3 cameraPos = {0.0f, 0.0f, 3.0f};  //摄像机的位置
static vec3 cameraFront = {0.0f, 0.0f, -1.0f}; //摄像机方向，z轴负方向
static vec3 cameraUp = {0.0f, 1.0f, 0.0f}; //摄像机上向量
static float cameraSpeed = 0.05f; //摄像机移动速度

//摄像机俯仰角（上下）与xz平面的夹角
static float pitch = 0;
//摄像机偏航角（左右）与z轴负(之前已经设置了cameraFront摄像机默认方向看向z轴负方向)方向的夹角
static float yaw = 0;

//鼠标是否是第一次移到屏幕中，默认是
static int firstMouse = true;
//记录鼠标上一次的位置，默认屏幕中心
static float lastX = (float )width/2;
static float lastY = (float )height/2;

static float fovy = 45.0f;//视野

//鼠标回调,改变摄像机的方向
static void mouse_callback(GLFWwindow* window, double x, double y);
//滚轮回调,当滚动鼠标滚轮的时候，y_offset值代表我们竖直滚动的大小
//当scroll_callback函数被调用后，我们改变全局变量fov变量的内容。因为45.0f是默认的视野值，我们将会把缩放级别(Zoom Level)限制在1.0f到45.0f。
static void scroll_callback(GLFWwindow* window, double x_offset, double y_offset);
/**
 * 输入事件监听，改变摄像机的位置
 */
static void processInput1(GLFWwindow *window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, 1);

    //朝设摄像机方向移动（前进）
    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS){
        vec3 frontScale = GLM_VEC3_ZERO_INIT;
        //数乘法，方向不变，大小改变（由移动速度确定）
        glm_vec3_scale(cameraFront, cameraSpeed, frontScale);
        //移动摄像机后 摄像机新的位置
        vec3 newPos = GLM_VEC3_ZERO_INIT;
        glm_vec3_add(cameraPos, frontScale, newPos);//向量+
        //新的位置拷贝给摄像机位置变量
        glm_vec3_copy(newPos, cameraPos);
    }
    //朝设摄像机反方向移动（后退）
    if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS){
        vec3 frontScale = GLM_VEC3_ZERO_INIT;
        //数乘，方向不变，大小改变（由移动速度确定）
        glm_vec3_scale(cameraFront, cameraSpeed, frontScale);
        //移动摄像机后 摄像机新的位置
        vec3 newPos = GLM_VEC3_ZERO_INIT;
        glm_vec3_sub(cameraPos, frontScale, newPos); //向量-
        //新的位置拷贝给摄像机位置变量
        glm_vec3_copy(newPos, cameraPos);
    }
    //朝设摄像机方向 左移动
    if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS){
        //得到向左的方向量:上向量叉乘摄像机的方向向量(不能交换顺序)
        vec3  left = GLM_VEC3_ZERO_INIT;
        glm_vec3_cross(cameraUp, cameraFront, left);
        glm_normalize(left); //进行标准化,如果我们没对这个向量进行标准化，最后的叉乘结果会根据cameraFront变量返回大小不同的向量
        //printVec3(left); //-1.000000, 0.000000, 0.000000
        //数乘，方向不变，大小改变（由移动速度确定）
        vec3 leftScale = GLM_VEC3_ZERO_INIT;
        glm_vec3_scale(left, cameraSpeed, leftScale);

        //移动摄像机后 摄像机新的位置
        vec3 newPos = GLM_VEC3_ZERO_INIT;
        glm_vec3_add(cameraPos, leftScale, newPos); //向量+
        //新的位置拷贝给摄像机位置变量
        glm_vec3_copy(newPos, cameraPos);
    }

    //朝设摄像机方向 右移
    if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS){
        //得到向右的方向量:摄像机的方向向量 叉乘 上向量(不能交换顺序)
        vec3  right = GLM_VEC3_ZERO_INIT;
        glm_vec3_cross(cameraFront, cameraUp, right);
        glm_normalize(right); //进行标准化,如果我们没对这个向量进行标准化，最后的叉乘结果会根据cameraFront变量返回大小不同的向量
        //printVec3(right); //1.000000, 0.000000, 0.000000
        //数乘，方向不变，大小改变（由移动速度确定）
        vec3 leftScale = GLM_VEC3_ZERO_INIT;
        glm_vec3_scale(right, cameraSpeed, leftScale);

        //移动摄像机后 摄像机新的位置
        vec3 newPos = GLM_VEC3_ZERO_INIT;
        glm_vec3_add(cameraPos, leftScale, newPos); //向量+
        //新的位置拷贝给摄像机位置变量
        glm_vec3_copy(newPos, cameraPos);
    }



}
/**
 * 设置链接顶点属性
 */
static void setVertexAttr()
{
    /*
    第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗？它可以把顶点属性的位置值设置为0。因为我们希望把数据传递到这一个顶点属性中，所以这里我们传入0。
    第二个参数指定顶点属性的大小。顶点属性是一个vec3，它由3个值组成，所以大小是3。
    第三个参数指定数据的类型，这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。
    下个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE，所有数据都会被映射到0（对于有符号型signed数据是-1）到1之间。我们把它设置为GL_FALSE。
    第五个参数叫做步长(Stride)，它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个GLfloat之后，我们把步长设置为3 * sizeof(GLfloat)。要注意的是由于我们知道这个数组是紧密排列的（在两个顶点属性之间没有空隙）我们也可以设置为0来让OpenGL决定具体步长是多少（只有当数值是紧密排列时才可用）。一旦我们有更多的顶点属性，我们就必须更小心地定义每个顶点属性之间的间隔，我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。
    最后一个参数的类型是GLvoid*，所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头，所以这里是0
    */

    // position attribute 位置坐标
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    // texture coord attribute 纹理坐标
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);


}

/**
 * 变换（平移,旋转）模型矩阵
 * @param shaderProgram
 * @param angel 旋转角度
 * @param rotateV 旋转轴向量
 * @param translateV 平移向量
 */
static void modelMat(GLuint shaderProgram, float angel, vec3 rotateV, vec3 translateV){
    mat4 model = GLM_MAT4_IDENTITY_INIT; //定义一个变换矩阵 单位矩阵


    glm_translate(model, translateV);//平移变换
    glm_rotate(model, angel/180.0f * CGLM_PI, rotateV);//旋转

    unsigned int transformLoc = glGetUniformLocation(shaderProgram, "model");
    glUniformMatrix4fv(transformLoc, 1, GL_FALSE, (const GLfloat *) model);
}

/**
 * 观察矩阵 使用lookAt创建 模拟摄像机围绕原点旋转的效果
 * @param shaderProgram
 */
static void viewMat(GLuint shaderProgram){

    mat4 view = GLM_MAT4_IDENTITY_INIT; //定义一个变换矩阵 单位矩阵
    vec3 target = GLM_VEC3_ZERO_INIT;
    //摄像机位置 + 摄像机方向 = 目标位置
    glm_vec3_add(cameraPos, cameraFront, target);
    //LookAt函数需要一个位置、目标和上向量
    glm_lookat(cameraPos, target, cameraUp, view);
    unsigned int transformLoc = glGetUniformLocation(shaderProgram, "view");
    glUniformMatrix4fv(transformLoc, 1, GL_FALSE, (const GLfloat *) view);

}

/**
 * 投影矩阵 使用透视投影
 * @param shaderProgram
 */
static void projectionMat(GLuint shaderProgram){
    mat4 projection = GLM_MAT4_IDENTITY_INIT; //定义一个变换矩阵 单位矩阵
    glm_perspective(fovy, (float )width/(float )height, 0.1f, 100.0f, projection);
    unsigned int transformLoc = glGetUniformLocation(shaderProgram, "projection");
    glUniformMatrix4fv(transformLoc, 1, GL_FALSE, (const GLfloat *) projection);

}





int main14(void)
{
    //初始化，创建窗口
    GLFWwindow *window = initWindow(width, height, "LearnOpenGL");

    //调用这个函数之后，无论我们怎么去移动鼠标，光标都不会显示了，它也不会离开窗口。对于FPS摄像机系统来说非常完美。
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
    //注册鼠标回调函数
    glfwSetCursorPosCallback(window, mouse_callback);
    //注册鼠标滚轮的回调函数
    glfwSetScrollCallback(window, scroll_callback);
    /**
     * OpenGL存储它的所有深度信息于一个Z缓冲(Z-buffer)中，也被称为深度缓冲(Depth Buffer)。GLFW会自动为你生成这样一个缓冲（就像它也有一个颜色缓冲来存储输出图像的颜色）。深度值存储在每个片段里面（作为片段的z值），当片段想要输出它的颜色时，OpenGL会将它的深度值和z缓冲进行比较，如果当前的片段在其它片段之后，它将会被丢弃，否则将会覆盖。这个过程称为深度测试(Depth Testing)，它是由OpenGL自动完成的
     **/
    //开启深度测试
    glEnable(GL_DEPTH_TEST);
    //创建着色器程序
    //编译着色器源码，创建着色器程序
    GLuint shaderProgram = compileShaderSource("D:\\CLionProjects\\opengl-mingw\\shader\\shader12.vs",
                                               "D:\\CLionProjects\\opengl-mingw\\shader\\shader12.fs");

    GLuint VBO = createVBO(vertices, sizeof (vertices) / sizeof (float ));
    GLuint VAO = createVAO();
    setVertexAttr();
    //创建纹理 第一个

    GLuint texture1 = createTexture("D:\\CLionProjects\\opengl-mingw\\img\\container.jpg", GL_RGB, GL_RGB);
    //创建纹理 第二个
    GLuint texture2 = createTexture("D:\\CLionProjects\\opengl-mingw\\img\\awesomeface.png", GL_RGBA, GL_RGBA);



    //在设置uniform变量之前，激活着色器程序
    glUseProgram(shaderProgram); // don't forget to activate/use the shader before setting uniforms!
    //使用glUniform1i设置每个采样器的方式告诉OpenGL每个着色器采样器属于哪个纹理单元
    //只需要设置一次即可，所以这个会放在渲染循环的前面

    glUniform1i(glGetUniformLocation(shaderProgram, "texture1"), 0);
    glUniform1i(glGetUniformLocation(shaderProgram, "texture2"), 1);



    //一般当你打算绘制多个物体时，你首先要生成/配置所有的VAO（和必须的VBO及属性指针)，然后储存它们供后面使用。当我们打算绘制物体的时候就拿出相应的VAO，绑定它，绘制完物体后，再解绑VAO。
    while (!glfwWindowShouldClose(window))
    {
        processInput1(window);
        //设置清除颜色
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        //使用上面设置的颜色清空屏幕
       // glClear(GL_COLOR_BUFFER_BIT);
        //因为使用了深度测试，我们也想要在每次渲染迭代之前清除深度缓冲（否则前一帧的深度信息仍然保存在缓冲中）。就像清除颜色缓冲一样
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        //绑定纹理第一个
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, texture1);
        //绑定纹理第二个
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, texture2);


        //调用glUseProgram函数，用刚创建的程序对象作为它的参数，以激活这个程序对象
        glUseProgram(shaderProgram);

        //观察矩阵
        viewMat(shaderProgram);
        //投影矩阵
        projectionMat(shaderProgram);
        //绑定VAO
        glBindVertexArray(VAO);

        vec3 rotateV = {1.0f, 0.3f, 0.5f};
        //画10次
        for(int i = 0; i < 10; i++){
            //设置平移位置和旋转角度
            modelMat(shaderProgram, (float)glfwGetTime() * 20.0f * (float )(i+1), rotateV, cubePositions[i]);
            glDrawArrays(GL_TRIANGLES, 0, 36);
        }


        //交换颜色缓冲区
        glfwSwapBuffers(window);
        //检查事件
        glfwPollEvents();
    }
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    //glfwTerminate会销毁窗口释放资源，因此在调用该函数后，如果想使用glfw库函数，就必须重新初始化。
    glfwTerminate();
    return 0;
}

//鼠标回调,改变摄像机的方向
static void mouse_callback(GLFWwindow* window, double x, double y){
    /**
     * 如果你现在运行代码，你会发现在窗口第一次获取焦点的时候摄像机会突然跳一下。这个问题产生的原因是，在你的鼠标移动进窗口的那一刻，
     * 鼠标回调函数就会被调用，这时候的xpos和ypos会等于鼠标刚刚进入屏幕的那个位置。这通常是一个距离屏幕中心很远的地方，因而产生一个很大的偏移量，
     * 所以就会跳了。我们可以简单的使用一个bool变量检验我们是否是第一次获取鼠标输入，如果是，那么我们先把鼠标的初始位置更新为xpos和ypos值，这样就能解决这个问题
     **/
    if(firstMouse){
        lastX = (float )x;
        lastY = (float )y;
        firstMouse = false;
    }

    float x_offset = (float )x - lastX;
    float y_offset = -((float)y - lastY); //鼠标往上移动，屏幕的y坐标减小，而世界坐标中，网上是y增大，所以取反

    lastX = (float )x;
    lastY = (float )y;

    float sensitivity = 0.05f;//灵敏度
    x_offset *= sensitivity;
    y_offset *= sensitivity;

    yaw += x_offset;
    pitch += y_offset;
    //对于俯仰角，要让用户不能看向高于89度的地方（在90度时视角会发生逆转，所以我们把89度作为极限），同样也不允许小于-89度。这样能够保证用户只能看到天空或脚下，但是不能超越这个限制
    if(pitch > 89.0f)
        pitch =  89.0f;
    if(pitch < -89.0f)
        pitch = -89.0f;
    //注意我们没有给偏航角设置限制，这是因为我们不希望限制用户的水平旋转。当然，给偏航角设置限制也很容易，如果你愿意可以自己实现。
    if(yaw > 89.0f)
        yaw =  89.0f;
    //可以设置一下偏航角的范围
    if(yaw < -89.0f)
        yaw = -89.0f;

    //通过俯仰角和偏航角来计算以得到真正的方向向量

    cameraFront[0] = cosf(pitch/180.0f * CGLM_PI) * sinf(yaw/180.0f * CGLM_PI) ;
    cameraFront[1] = sinf(pitch/180.0f * CGLM_PI);
    cameraFront[2] = cosf(pitch/180.0f * CGLM_PI) * cosf(yaw/180.0f * CGLM_PI) * (-1); //取反表示看向z轴负方向
    glm_normalize(cameraFront);
}

static void scroll_callback(GLFWwindow* window, double x_offset, double y_offset)
{
    float sensitivity = 0.05f;//灵敏度
    y_offset *= sensitivity;
    if(fovy >= 1.0f && fovy <= 45.0f)
        fovy -= (float)y_offset;
    if(fovy <= 1.0f)
        fovy = 1.0f;
    if(fovy >= 45.0f)
        fovy = 45.0f;
}